iT邦幫忙

2023 iThome 鐵人賽

DAY 3
1
Modern Web

從Vue學React!不只要會用,還要真的懂~系列 第 3

【Day 3】可變特性與不可變特性&渲染的觸發與Virtual DOM的產生

  • 分享至 

  • xImage
  •  

不論是學習Vue或是學習React,除了學會怎麼使用它們的API或hook外,還必須了解它的渲染機制,這樣才可以避免出現一些預期外的bug而不自知,或是在需要某些特別的操作時,不知道該如何下手。今天就來延伸前面所提過的mutable和immutable,來感受一下Vue和React在觸發渲染上的差異吧!

這裡指的渲染render和重新渲染re-render是?

在進入正題之前,我們先來快速定義一下包含這個章節,以及接下來的其他篇章節出現的「渲染」及「重新渲染」代表的意思。大家聽到渲染,或是畫面渲染的這個詞,大多數的人下意識都會覺得是「瀏覽器渲染」,也就是我們眼睛看得到的網頁畫面被呈現出來的過程。但是我們這裡說到的渲染並不是「瀏覽器渲染」,而是呼叫render function或component function來產生virtual DOM的動作。

什麼是Virtual DOM?

接下來再來快速複習一下我們可能都聽過的Virtual DOM。在網頁畫面呈現在電腦螢幕上之前,瀏覽器會把HTML會被轉換成DOM tree,這是跟畫面呈現有關的資料結構。而Virtual DOM則不是由瀏覽器產生的DOM,Virtual DOM是由JavaScript產生用來描述實體DOM的物件。有了Virtual DOM,就能在畫面更新時,藉由比較新舊Virtual DOM之間的差異,來改動瀏覽器產生的DOM的方式,進而減少畫面更新實體DOM所需要花費的效能。不論是Vue還是React都是透過Virtual DOM的概念來減少重新渲染所需要耗的效能,但是這與昨天提到的mutable和immutable究竟有什麼關聯呢?

Vue和以可變特性觸發Virtual DOM產生的渲染機制

在Vue中,若是響應的state,就會被Vue監聽,當state有變動,會確認這個state有無使用到template中,如果有使用到template上,就會開始進行「重新渲染(re-render)」。這裡指的「重新渲染」是指「依照template的內容,產生新的Virtual DOM的動作,再和舊的Virtual DOM進行比對,接著將比對出來有不同的部分更新到實體的DOM」的過程。監聽state是否有變動的這個部分,是使用到mutable的特性,也就是說只會去比對值有沒有不同,不會進一步去看它們的記憶體位址是否真的有不同。這裡還有一個部分需要特別提的是「這裡提到的依照template的內容,產生新的virtual DOM」的部分,進行動作的只有template的區塊,script區塊的setup()只有在初始化階段會執行一次

https://ithelp.ithome.com.tw/upload/images/20230909/20130914CnbxVfRX1w.png
(圖片來源: https://vuejs.org/guide/extras/rendering-mechanism.html#render-pipeline)

用一句話來說明的話,也就是「監聽使用在template中的mutable state的改動,來觸發render function的呼叫,進而產生新的virtual DOM」。

這裡直接用實際的程式碼來看一下!
情境一:這裡是一個mutable的state,把它用在template上,並且透過按鈕變更state。

<script setup>
import { ref, onUpdated } from 'vue';
console.log('setup');
// 在ref包裝的使用下,number會變成mutable的變數
const number = ref(1);

const addNumber = () => {
  number.value ++;
  console.log(number.value);
};

onUpdated(() => {
  console.log('updated');
});

</script>

<template>
  <main>
    <p>number: {{ number }}</p>
    <button @click="addNumber">change</button>
  </main>
</template>

https://i.imgur.com/V7z7Fwn.gif

情境二:這裡有一個沒有用在template上的mutable state,但仍然透過按鈕變更state。

<template>
  <main>
    // 把原本有使用到ref state的地方移除掉,畫面已經不會顯示number
    <button @click="addNumber">change</button>
  </main>
</template>

https://i.imgur.com/pIveAAr.gif

從上面兩個情境中,可以觀察到以下這幾個Vue渲染的特點

  • 重新渲染的動作只會執行template的部份,所以寫在setup中的console.log並不會再次被印出,但當有畫面更新時,生命週期的方法會被觸發,所以onUpdated中的console.log會被印出。
  • 只有使用到template中的state變動才會觸發重新渲染,所以當改動沒有使用在template中的state時,並不會看到'updated'被console.log出來。
  • 只有在新舊virtual DOM被比對後,確認出現差異時,才會出現將新的內容繪製上去的動作,也就是我們肉眼看到對應element閃一下的部分。

可以使用immutable的state來觸發Vue渲染嗎?

如果想要讓Vue能夠監聽資料是否有改動,就必須使用ref或reactive來攔截資料的讀取和寫入,而ref和reactive使用到的特性就是mutable。

這裡我們可以自己宣告的一個immutable state來實驗看看。

<script setup>
import { onUpdated } from 'vue';
// 一般的immutable變數
let immutableNumber = 1;

const addNumber = () => {
  immutableNumber ++
  console.log(immutableNumber);
};

onUpdated(() => {
  console.log('updated');
});
</script>

<template>
  <main>
    <p>number: {{ immutableNumber }}</p>
    <button @click="addNumber">change</button>
  </main>
</template>

https://i.imgur.com/tB6mrJM.gif

這裡可以看到雖然數字增加了,但畫面並沒有任何變動。這也就說明了immutable變數無法觸發畫面重新渲染。另外,需要再次強調的是Vue的響應式state都是mutable特性的變數。這時候你可能還會有一個疑問,所以一定要用一般的mutable變數不行嗎?我們可以再用一個實際的程式碼來實驗看看!

這裡我們自己宣告一個mutable變數。

<script setup>
import { onUpdated } from 'vue';
// 一般的mutable變數
const normalMutableNumber = {
  val: 1,
};

const addNumber = () => {
  normalMutableNumber.val ++;
  console.log(normalMutableNumber.value);
};

onUpdated(() => {
  console.log('updated');
});
</script>

<template>
  <main>
    <p>number: {{ normalMutableNumber.val }}</p>
    <button @click="addNumber">change</button>
  </main>
</template>

https://i.imgur.com/3nviA6F.gif

結果一樣不會觸發任何重新渲染,因為只有Vue的響應式的state可以讓Vue去監控state有無變動,也才會觸發重新渲染。

React和與不可變特性有關聯的渲染機制

在React中,會在我們手動呼叫setState時,對state進行變更,在這個時候也就會讓React知道我們正在更動state,但這個改動並不會直接修改原本的state的值,而是透過創建新的state並且重新賦值方式進行state的更新,以確保state為immutable。當呼叫setState進行state的改動後,並不會立即進行重新渲染的動作,React還會用Object.is檢查新的state和舊的state有沒有不同,真的確認結果為不同時,才會進行「重新渲染(re-render)」。這裡提到的「重新渲染」指的是「呼叫component function產生出新的virtual DOM」的動作,在產生新的virtual DOM後,會進行「Reconciliation」,也就是「讓新舊的virtual DOM進行比對,當比對出有差異時,再針對有差異部分的來更新實體的DOM」過程,最後才會將最終更新的結果繪製到畫面上。

用一句話來說明React的渲染機制的話,就是「React在setState時,會進行讓state以維持immutable特性的方式進行state的更新,接著會檢查新舊state是否為不同,不同時就會真正地進入重新渲染和Reconciliation的過程,最後把結果繪製上畫面」。

這裡也透過實際的程式碼來看看這個渲染機制下的情況來觀察React渲染的特點!
情境一:這裡是一個有使用在畫面上的immutable的state,透過按鈕變更state。

import { useState } from 'react';

function App() {
  // 使用useState回傳的immutable變數
  const [number, setNumber] = useState(1);
  const handleAddNumber = () => {
    setNumber(number + 1);
  };
  console.log('update');
  return (
    <div className="App">
      <div>
        <p>number: {number}</p>
        <button onClick={handleAddNumber}>add</button>
      </div>
    </div>
  );
}

export default App;

https://i.imgur.com/OVyeac0.gif

情境二:有一個沒有被使用到的immutable的state,透過按鈕變更這個state。

import { useState } from 'react';

function App() {
  // 使用useState回傳的immutable變數
  const [number, setNumber] = useState(1);
  const handleAddNumber = () => {
    setNumber(number + 1);
  };
  console.log('update');
  return (
    <div className="App">
      <div>
        {/* 把這裡註解調,變成沒有使用這個變數 */}
        {/* <p>number: {number}</p> */}
        <button onClick={handleAddNumber}>add</button>
      </div>
    </div>
  );
}

export default App;

https://i.imgur.com/qfpEOhe.gif

從上面兩個情境中,可以觀察到以下這幾個React渲染的特點

  • 重新渲染指的是重新呼叫component function,所以只要透過setState對state進行變更,並且比確認值有變更時,'update'就會被console.log。
  • 即便state沒有使用在畫面上,因為一樣有進行setState的動作,且state也確實有變動,所以還是會進入重新渲染的動作,也就是「呼叫component function產生出新的virtual DOM」,所以可以觀察到'update'會被console.log出來。
  • 只有在新舊virtual DOM被比對後,確認出現差異時,才會出現將新的內容繪製上去的動作,也就是我們肉眼看到對應element閃一下的部分。

小補充:開發環境在嚴格模式下會渲染兩次

從前面的情境中,有沒有發現一個奇妙的地方呢?就是明明我們只要印出'update'一次,卻每次呼叫component function都會被印出兩次,這是為什麼呢?

那是因為現在是在開發環境下使用strict mode。

<StrictMode>
  <App />
</StrictMode>

在開發環境下使用strict mode的話,會模擬「mount -> unmount -> mount」的流程來避免一些bug的出現,讓我們能及早查覺到有問題,所以才會在前面的範例中看到每改動一次state,就印出兩次update的狀況。

可以使用mutable的state來觸發React渲染嗎?

和Vue一樣,如果想觸發React重新產生新的virtual DOM來進行比對及更新不同的地方,就必須使用React提供的useState或useReducer hooks,且並須透過setState通知React我們現在正在進行對state的改動。所以如果不論是直接使用useRef宣告一個mutable特性的變數,或是宣告一個單純的immutable變數,並且對這些變數進行變更,都不會觸發React進行重新渲染的動作。

import { useRef } from 'react';

function App() {
  // 使用useRef的mutable變數
  const mutableNumber = useRef(1);
  const handleAddNumber = () => {
    mutableNumber.current ++;
    console.log(mutableNumber.current);
  };
  console.log('update');
  return (
    <div className="App">
      <div>
        <p>{mutableNumber.current}</p>
        <button onClick={handleAddNumber}>add</button>
      </div>
    </div>
  );
}

export default App;

https://i.imgur.com/AC60vgy.gif
在這個情境中可以觀察到雖然數字不斷地有增加,但是並未觸發重新渲染,所以update只有印出一開始第一次渲染畫面的那兩次。

一般的immutable變數也會出現一樣的情況。

function App() {
  let normalImmutableNumber = 1;
  const handleAddNumber = () => {
    normalImmutableNumber ++;
    console.log(normalImmutableNumber);
  };
  console.log('update');
  return (
    <div className="App">
      <div>
        <p>{normalImmutableNumber}</p>
        <button onClick={handleAddNumber}>add</button>
      </div>
    </div>
  );
}

export default App;

https://i.imgur.com/j4ABBHN.gif

從結果就可以發現,只有使用React提供的hook下去管理state,會觸發重新渲染。

小總結

最後透過一個總結來回顧一下這個章節的重點內容吧!

  • Vue是監聽mutable特性的變數來觸發渲染,React則是透過setState這個讓state維持immutable的state更新方法來通知React觸發渲染。
  • Vue跟React都必須使用它們提供的狀態管理API或hooks來管state,才能觸發渲染,不是單純的mutable或immutable變數就能觸發渲染。
  • Vue觸發重新渲染時,只會執行template的部分,不會再次執行setup;React重新渲染時則會重新呼叫整個component function。

不論是React或是Vue,想要觸發元件進行重新渲染產生新的virtual DOM,都需要讓state被管理在它們的體制之中,例如使用ref、useState來宣告state。不同的是,Vue的state是利用mutable的特性,讓操作修改時,能夠更直觀地進行改動;React則是透過setState讓state維持immutable的特性,讓所有改動都能在自己預期內。這些渲染特性都是學習Vue和React時,必須要了解的部分,今天看完了可變特性與不可變特性和觸發畫面渲染的關係後,明天繼續來看與今天說的內容有關聯的「Reconciliation」吧!

參考資料

[Vue]Rendering Mechanism - Render Pipeline
[React]Render and Commit
[React]StrictMode
[React]Adding Reusable State to StrictMode


上一篇
【Day 2】這趟旅程,從認識mutable和immutable開始
下一篇
【Day 4】 觸發重新渲染後的下一步 - Reconciliation (上)
系列文
從Vue學React!不只要會用,還要真的懂~30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Zet
iT邦新手 2 級 ‧ 2023-09-11 04:08:52

「在React中則是透過immutable特性下去觸發render」以及「重新渲染指的是重新呼叫component function,所以只要state一有變動,'update'就會被console.log。」

這個部分的理解有一些小問題,immutable 的資料是本身無法做到監聽效果的,React 並不像 Vue 一樣會「監聽」state 的變化,所有的 reconciliation 流程都是透過手動呼叫 setState 方法而觸發的。

所以並不是「state 一有變動就會自動觸發 re-render」,而是因為你自己呼叫了 setState 方法所以才觸發 re-render 的,也就是說其實是你自己通知 React 有資料發生變化的。React 所謂的「state 檢查」只是在你呼叫 setState 方法時檢查你傳入的參數與舊有的 state 是否相同以避免無謂的效能浪費而已。

感謝Zet大大幫我糾正出理解錯誤的部分,已經重新思考和消化自己誤解的部分並且修改更新上去了(_ _)

Vue會自動攔截監聽state的更新,來觸發畫面渲染。
React則是在進行setState的時候,會讓React知道我們有對state進行更動,然後會再進一步檢查新舊state是不是真的有不同,確認不同才進行重新渲染的動作。

我要留言

立即登入留言